﻿// <copyright file="TTableStorageEngine.cs" company="N/A">
//     Copyright (c) Nicholas Barrett and Benjamin Kasten.  All rights reserved.
//
//     This program is free software; you can redistribute it and/or modify it
//     under the terms of the GNU General Public License as published by the
//     Free Software Foundation; either version 2 of the License, or (at your
//     option) any later version.
//
//     This program is distributed in the hope that it will be useful, but
//     WITHOUT ANY WARRANTY; without even the implied warranty of
//     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
//     Public License for more details.
//
//     You should have received a copy of the GNU General Public License along
//     with this program; if not, write to the Free Software Foundation, 
//     Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
// </copyright>

namespace BetterAzureTableStorage
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using System.Data.Services.Client;
    using System.Globalization;
    using System.IO;
    using System.Linq;
    using System.Net;
    using System.Reflection;
    using System.Text;
    using System.Threading;
    using System.Xml.Linq;
    using Microsoft.WindowsAzure;
    using Microsoft.WindowsAzure.StorageClient;
    using System.Linq.Expressions;

    /// <summary>
    /// This class will be instantiated to interact with the Azure Table Storage in a REST-ful fashion
    /// </summary>
    /// <remarks>
    /// TTableStorageEngine will handle all interactions to table storage including table creation, 
    /// queries, inserts, updates, and deletes.
    /// </remarks>
    public class TTableStorageEngine
    {
        /// <summary>
        /// The Atom namespace in string format.
        /// </summary>
        private static string atomNStext = @"http://www.w3.org/2005/Atom";

        /// <summary>
        /// The Atom namespace.
        /// </summary>
        private static XNamespace atomNS = atomNStext;

        /// <summary>
        /// The data service metadata namespace.
        /// </summary>
        private static XNamespace metaNS = @"http://schemas.microsoft.com/ado/2007/08/dataservices/metadata";

        /// <summary>
        /// The data service namespace.
        /// </summary>
        private static XNamespace dataNS = @"http://schemas.microsoft.com/ado/2007/08/dataservices";

        /// <summary>
        /// Object to be locked when adding to the ParseMethods dictionary.
        /// </summary>
        private static object fParseMethodsLockObject = new object();

        /// <summary>
        /// Gets or sets the MethodInfo for the Parse message of a given type.
        /// </summary>
        /// <remarks>You must use the fParseMethodsLockObject when adding to this dictionary.</remarks>
        private static Dictionary<Type, MethodInfo> ParseMethods { get; set; }

        /// <summary>
        /// Gets or sets the credentials used to sign requests to the table service.
        /// </summary>
        private StorageCredentials Credentials { get; set; }

        /// <summary>
        /// Gets or sets the service root URI.
        /// </summary>
        /// <value>The table service URI.</value>
        private Uri ServiceRoot { get; set; }

        /// <summary>
        /// Gets or sets the list of table storage info objects passed in from the context.
        /// </summary>
        /// <remarks>
        /// There should be a TTableStorageInfo object for each type of entity in the transaction.
        /// There should only be one table within a given TableInfoList per instance of the engine.
        /// </remarks>
        /// <value>The table info list.</value>
        private List<TTableStorageInfo> TableInfoList { get; set; }

        /// <summary>
        /// Gets or sets the name of the table.
        /// </summary>
        /// <value>The name of the table.</value>
        private string TableName { get; set; }

        /// <summary>
        /// Gets or sets the retry policy.
        /// </summary>
        /// <value>The retry policy.</value>
        private RetryPolicy RetryPolicy { get; set; }

        /// <summary>
        /// Gets or sets an action to be called when the transaction count is incremented.
        /// </summary>
        private Action TransactionCountIncremented { get; set; }

        #region reflection fields
        private readonly MethodInfo CreateQueryMethodDefinition = typeof(TTableStorageDataContext).GetMethod("CreateQuery");

        private readonly MethodInfo WhereMethodDefinition = typeof(Queryable).GetMethods().Single(m => m.ToString() == "System.Linq.IQueryable`1[TSource] Where[TSource](System.Linq.IQueryable`1[TSource], System.Linq.Expressions.Expression`1[System.Func`2[TSource,System.Boolean]])");

        private readonly Type DelegateTypeDefinition = typeof(Func<,>);

        private readonly MethodInfo LambdaMethodDefinition = typeof(TTableStorageEngine).GetMethod("LambdaHelper");

        private readonly Type QueryableTypeDefinition = typeof(IQueryable<>);

        private readonly Type TSQueryDefinition = typeof(TTableStorageQuery<>);
        #endregion

        /// <summary>
        /// Initializes a new instance of the <see cref="TTableStorageEngine"/> class.
        /// </summary>
        /// <param name="serviceRoot">The table storage service root Uri.</param>
        /// <param name="credentials">The credentials for accessing table storage.</param>
        /// <param name="tableInfoList">The table info list.</param>
        /// <param name="tableName">Name of the table.</param>
        /// <param name="retryPolicy">The retry policy.</param>
        /// <param name="onTransactionCountIncremented">The action to be called when the transaction count is incremented.</param>
        public TTableStorageEngine(Uri serviceRoot,
            StorageCredentials credentials,
            List<TTableStorageInfo> tableInfoList,
            string tableName,
            RetryPolicy retryPolicy,
            Action onTransactionCountIncremented)
        {
            this.ServiceRoot = serviceRoot;
            this.Credentials = credentials;

            this.TableInfoList = tableInfoList;
            this.TableName = tableName;

            this.RetryPolicy = retryPolicy;

            this.TransactionCountIncremented = onTransactionCountIncremented;

            ParseMethods = new Dictionary<Type, MethodInfo>();
        }

        /// <summary>
        /// Creates an HttpWebRequest for a batched table request.
        /// </summary>
        /// <param name="targets">The targets of each update request.</param>
        /// <returns>
        /// An HttpWebRequest for a batched table request.
        /// </returns>
        public HttpWebRequest CreateEntityBatchRequest(IEnumerable<TTableEntityTransactionInfo> targets)
        {
            if (targets == null)
            {
                throw new ArgumentNullException("targets");
            }

            if (targets.Count() == 0)
            {
                throw new ArgumentException("Targets must contain at least one element to generate a request.");
            }

            Guid batchID = Guid.NewGuid();
            Guid changesetID = Guid.NewGuid();

            int contentID = 1;

            HttpWebRequest request = this.CreateRequest(new Uri(UriHelper.Combine(this.ServiceRoot.ToString(), @"$batch")));

            // This is always POST, each batch entry has its own verb.
            request.Method = HttpConstants.Post;
            request.ContentType = string.Format("multipart/mixed; boundary=batch_{0}", batchID);

            using (MemoryStream ms = new MemoryStream())
            {
                StreamWriter sw = new StreamWriter(ms);

                sw.WriteLine("--batch_{0}", batchID.ToString());
                sw.WriteLine("Content-Type: multipart/mixed; boundary=changeset_{0}", changesetID.ToString());
                sw.WriteLine();

                foreach (TTableEntityTransactionInfo target in targets)
                {
                    sw.WriteLine("--changeset_{0}", changesetID.ToString());
                    sw.WriteLine("Content-Type: application/http");
                    sw.WriteLine("Content-Transfer-Encoding: binary");
                    sw.WriteLine();

                    target.ContentID = contentID++;

                    switch (target.Verb)
                    {
                        case HttpConstants.Put:
                        case HttpConstants.Post:
                            XDocument doc = new XDocument(new XDeclaration("1.0", "utf-8", "yes"), this.CreateDataServiceEntry(target.Entity));
                            char[] charData = (doc.Declaration.ToString() + Environment.NewLine + doc.ToString()).ToCharArray();

                            if (target.Verb == HttpConstants.Post)
                            {
                                sw.WriteLine("{0} {1} HTTP/1.1", target.Verb, UriHelper.Combine(this.ServiceRoot.ToString(), this.TableName));
                            }
                            else
                            {
                                sw.WriteLine("{0} {1} HTTP/1.1", target.Verb,
                                    UriHelper.Combine(this.ServiceRoot.ToString(), this.TableName) + string.Format("(PartitionKey='{0}',RowKey='{1}')", target.Entity.PartitionKey, target.Entity.RowKey));
                                sw.WriteLine("If-Match: {0}", "*");
                            }

                            sw.WriteLine("Content-ID: {0}", target.ContentID);
                            sw.WriteLine("Content-Type: application/atom+xml;type=entry");
                            sw.WriteLine("Content-Length: {0}", charData.Length);
                            sw.WriteLine();

                            sw.WriteLine(charData);
                            break;

                        case HttpConstants.Delete:
                            sw.WriteLine("DELETE {0} HTTP/1.1",
                                UriHelper.Combine(this.ServiceRoot.ToString(), this.TableName) + string.Format("(PartitionKey='{0}',RowKey='{1}')", target.Entity.PartitionKey, target.Entity.RowKey));
                            sw.WriteLine("Content-ID: {0}", target.ContentID);
                            sw.WriteLine("If-Match: {0}", "*");
                            sw.WriteLine();
                            break;

                        default:
                            throw new TTableStorageException(string.Format("Unsupported method type '{0}'", target.Verb));
                    }
                }

                sw.WriteLine("--changeset_{0}--", changesetID.ToString());
                sw.WriteLine("--batch_{0}--", batchID.ToString());

                sw.Flush();
                request.ContentLength = ms.Length;
                byte[] byteData = new byte[ms.Length];
                ms.Seek(0L, SeekOrigin.Begin);
                ms.Read(byteData, 0, (int)ms.Length);

                request.GetRequestStream().Write(byteData, 0, byteData.Length);
            }

            return request;
        }

        /// <summary>
        /// Gets a list of entities from table storage matching the qryString filter.
        /// </summary>
        /// <typeparam name="ResultT">The type the entity.  This can be abstract.</typeparam>
        /// <param name="qryString">The OData filter string (?$filter=...)</param>
        /// <param name="continuationTokens">The continuation tokens.</param>
        /// <returns>
        /// An IList{ResultT}
        /// </returns>
        public IList<ResultT> GetEntities<ResultT>(TTableStorageQueryTranslator translator, string continuationTokens = null, int level = 0, TTableStorageDataContext context = null)
        {
            XDocument doc = null;
            string myTokens = String.Empty;

            IList<ResultT> myList = new List<ResultT>();
            bool notFound = false;

            Retry.Invoke(
                () =>
                {
                    if (continuationTokens != null)
                    {
                        continuationTokens = continuationTokens.Substring(1);

                        if (translator.TranslatedText.Contains('?'))
                        {
                            continuationTokens = "&" + continuationTokens;
                        }
                        else
                        {
                            continuationTokens = "?" + continuationTokens;
                        }
                    }

                    HttpWebRequest request = this.CreateRequest(
                        new Uri(
                            string.Format("{0}{1}{2}",
                                UriHelper.Combine(
                                    this.ServiceRoot.ToString(),
                                    this.TableName),

                                translator.TranslatedText,

                                continuationTokens ?? String.Empty)));

                    HttpWebResponse response = null;

                    try
                    {
                        response = this.TrackGetResponse(request);
                    }
                    catch (WebException ex)
                    {
                        if ((ex.Response as HttpWebResponse).StatusCode != HttpStatusCode.NotFound)
                        {
                            throw ex;
                        }

                        response = ex.Response as HttpWebResponse;
                        notFound = true;
                    }

                    ////if (response.StatusCode != HttpStatusCode.OK)
                    ////{
                    ////    throw new TTableStorageException(response.StatusDescription);
                    ////}

                    doc = XDocument.Load(response.GetResponseStream());

                    string nextPartitionKey = response.Headers["x-ms-continuation-NextPartitionKey"];
                    string nextRowKey = response.Headers["x-ms-continuation-NextRowKey"];
                    string nextTableName = response.Headers["x-ms-continuation-NextTableName"];

                    if (!String.IsNullOrEmpty(nextPartitionKey))
                    {
                        myTokens += "&NextPartitionKey=" + nextPartitionKey;
                    }
                    if (!String.IsNullOrEmpty(nextRowKey))
                    {
                        myTokens += "&NextRowKey=" + nextRowKey;
                    }
                    if (!String.IsNullOrEmpty(nextTableName))
                    {
                        myTokens += "&NextTableName=" + nextTableName;
                    }
                }, this.RetryPolicy);

            if (notFound)
            {
                return myList;
            }

            foreach (var entry in doc.Element(atomNS + "feed").Elements(atomNS + "entry"))
            {
                XElement discriminatorElement = entry.Element(atomNS + "content").Element(metaNS + "properties").Element(dataNS + TableStorageConstants.DiscriminatorFieldName);

                TTableStorageInfo tableInfo = null;

                if (discriminatorElement != null && !string.IsNullOrEmpty(discriminatorElement.Value))
                {
                    tableInfo = this.TableInfoList.Where(ti => ti.ConditionalValue == int.Parse(discriminatorElement.Value)).FirstOrDefault();
                }

                if (tableInfo == null)
                {
                    tableInfo = this.TableInfoList.FirstOrDefault();
                }

                Type classType = tableInfo.ClassType;

                object entity = Activator.CreateInstance(classType);

                this.Materialize(entity, entry);

                foreach (string path in translator.ExpansionPaths)
                {
                    if (path.Contains('/'))
                    {
                        continue;
                    }

                    PropertyInfo navProp = classType.GetProperty(path);

                    AssociationAttribute association = (AssociationAttribute)navProp.GetCustomAttributes(typeof(AssociationAttribute), false).FirstOrDefault();

                    PropertyInfo thisKeyProp = classType.GetProperty(association.ThisKey);
                    PropertyInfo otherKeyProp = navProp.PropertyType.GetEffectiveType().GetProperty(association.OtherKey);

                    object thisKey = thisKeyProp.GetValue(entity, null);

                    List<string> subPaths = new List<string>();

                    foreach (string subPath in translator.ExpansionPaths.Where(p => p.StartsWith(path)))
                    {
                        if (subPath.Contains('/'))
                        {
                            string newPath = subPath.Substring(subPath.IndexOf('/')+1);

                            if (!String.IsNullOrEmpty(newPath))
                            {
                                subPaths.Add(newPath);
                            }
                        }
                    }

                    MethodInfo createQueryMethod = CreateQueryMethodDefinition.MakeGenericMethod(navProp.PropertyType.GetEffectiveType());

                    IQueryable qry = (IQueryable)createQueryMethod.Invoke(context, new object[] { });

                    MethodInfo whereMethod = WhereMethodDefinition.MakeGenericMethod(navProp.PropertyType.GetEffectiveType());

                    Type delegateType = DelegateTypeDefinition.MakeGenericType(navProp.PropertyType.GetEffectiveType(), typeof(bool));

                    MethodInfo lambdaMethod = LambdaMethodDefinition.MakeGenericMethod(delegateType);

                    ParameterExpression xParam = Expression.Parameter(navProp.PropertyType.GetEffectiveType(), "x");

                    Expression lambdaExpression = (Expression)lambdaMethod.Invoke(this,
                        new object[] {
                                Expression.Equal(
                                    Expression.MakeMemberAccess(xParam, otherKeyProp),
                                    Expression.Constant(thisKeyProp.GetValue(entity, null))),

                                new List<ParameterExpression>() {
                                    xParam
                                }
                            });

                    Type queryableType = QueryableTypeDefinition.MakeGenericType(navProp.PropertyType.GetEffectiveType());

                    Type tsQuery = TSQueryDefinition.MakeGenericType(navProp.PropertyType.GetEffectiveType());

                    MethodInfo expandMethod = tsQuery.GetMethod("Expand");

                    qry = (IQueryable)expandMethod.Invoke(qry, new object[] { String.Join(",", subPaths) });

                    qry = (IQueryable)whereMethod.Invoke(null, new object[] {
                            qry,
                            lambdaExpression
                        });

                    object navValue = navProp.GetValue(entity, null);

                    IList navCollection = navValue as IList;

                    if (navCollection != null)
                    {
                        navCollection.Clear();

                        foreach (object obj in qry)
                        {
                            navCollection.Add(obj);
                        }
                    }
                    else
                    {
                        foreach (object obj in qry)
                        {
                            navProp.SetValue(entity, obj, null);
                            break;
                        }
                    }
                }

                if (!String.IsNullOrWhiteSpace(translator.SelectPath))
                {
                    PropertyInfo selectProp = entity.GetType().GetProperty(translator.SelectPath);

                    if (typeof(IEnumerable).IsAssignableFrom(selectProp.PropertyType))
                    {
                        IEnumerable manyProp = (IEnumerable)selectProp.GetValue(entity, null);
                        foreach (object obj in manyProp)
                        {
                            myList.Add((ResultT)obj);
                        }
                    }
                    else
                    {
                        myList.Add((ResultT)selectProp.GetValue(entity, null));
                    }
                }
                else if (translator.SelectNewExpression != null)
                {
                    object newValue;

                    Delegate fn = translator.SelectNewExpression.Compile();
                    newValue = fn.DynamicInvoke(entity);

                    myList.Add((ResultT)newValue);
                }
                else
                {
                    myList.Add((ResultT)entity);
                }
            }

            if (!String.IsNullOrEmpty(myTokens))
            {
                IList<ResultT> continuedList = this.GetEntities<ResultT>(translator, myTokens, level, context);

                foreach (var continuedItem in continuedList)
                {
                    if (typeof(ResultT).IsAssignableFrom(continuedItem.GetType()))
                    {
                        myList.Add(continuedItem);
                    }
                }
            }

            return myList;
        }

        public Expression<TDelegate> LambdaHelper<TDelegate>(Expression body, IEnumerable<ParameterExpression> parameters)
        {
            return Expression.Lambda<TDelegate>(body, parameters);
        }

        /// <summary>
        /// Reads the batch response.
        /// </summary>
        /// <param name="request">The request from which to read the response.</param>
        /// <param name="targets">The entities that were targetted by the <paramref name="request"/>.</param>
        public void ReadEntityBatchResponse(HttpWebRequest request, IEnumerable<TTableEntityTransactionInfo> targets)
        {
            HttpWebResponse response = this.TrackGetResponse(request);

            if (response.StatusCode == HttpStatusCode.Accepted)
            {
                IEnumerable<TBatchResponse> responses = TBatchResponse.FromHttpWebResponse(response);

                foreach (TTableEntityTransactionInfo target in targets)
                {
                    TBatchResponse targetResponse = responses.FirstOrDefault(r => r.ContentID == target.ContentID);

                    if (targetResponse == null && responses.Count() > 0)
                    {
                        //// Batch failed due to a problem with another ContentID
                        continue;
                    }

                    HttpStatusCode targetResponseCode = targetResponse.StatusCode;

                    if (target.SuccessCode == targetResponseCode || targetResponseCode == HttpStatusCode.NotFound)
                    {
                        if (target.OnSuccess != null)
                        {
                            target.OnSuccess(target);
                        }
                    }
                    else
                    {
                        target.Error = new WebException(targetResponse.StatusDescription);

                        if (target.OnFailure != null)
                        {
                            target.OnFailure(target);
                        }

                        throw target.Error;
                    }
                }
            }
            ////else
            ////{
            ////    throw new TTableStorageException();
            ////}

            response.Close();
        }

        /// <summary>
        /// Create the table associated with this engine.
        /// </summary>
        /// <returns>True or false depending on if the table was created or not (respectively)</returns>
        public bool CreateTable()
        {
            XNamespace r = @"http://www.w3.org/2005/Atom";
            XNamespace d = @"http://schemas.microsoft.com/ado/2007/08/dataservices";
            XNamespace m = @"http://schemas.microsoft.com/ado/2007/08/dataservices/metadata";

            XDocument doc = new XDocument(new XDeclaration("1.0", "utf-8", "yes"));

            XElement entryElement = new XElement(r + "entry",
                new XAttribute("xmlns", @"http://www.w3.org/2005/Atom"),
                new XAttribute(XNamespace.Xmlns + "d", d),
                new XAttribute(XNamespace.Xmlns + "m", m),
                new XElement(r + "title"),
                new XElement(r + "updated", DateTime.UtcNow.ToString("yyyy-MM-dd'T'HH:mm:ssZ")),
                new XElement(r + "id"));

            XElement authorElement = new XElement(r + "author",
                new XElement(r + "name"));
            entryElement.Add(authorElement);

            XElement contentElement = new XElement(r + "content",
                new XAttribute("type", "application/xml"));
            entryElement.Add(contentElement);

            XElement propElement = new XElement(m + "properties",
                new XElement(d + "TableName", this.TableName));
            contentElement.Add(propElement);

            doc.Add(entryElement);

            HttpWebRequest request = WebRequest.Create(new Uri(this.ServiceRoot.ToString() + "/Tables")) as HttpWebRequest;

            this.Credentials.SignRequestLite(request);

            request.Date = DateTime.UtcNow;
            request.Method = HttpConstants.Post;
            request.ContentType = "application/atom+xml";

            using (StreamWriter sw = new StreamWriter(request.GetRequestStream()))
            {
                sw.Write(doc.Declaration.ToString() + doc.ToString());
            }

            HttpWebResponse response = this.TrackGetResponse(request);
            response.Close();

            return response.StatusCode == HttpStatusCode.Created;
        }

        /// <summary>
        /// Gets the parse method.
        /// </summary>
        /// <param name="type">The type to find a parse method for.</param>
        /// <returns>A MethodInfo object that will be used to parse a string into the <paramref name="type"/> Type.</returns>
        private MethodInfo GetParseMethod(Type type)
        {
            MethodInfo parseMethod;
            if (!ParseMethods.TryGetValue(type, out parseMethod))
            {
                parseMethod = type.GetMethods(BindingFlags.Static | BindingFlags.Public)
                    .FirstOrDefault(
                    m => m.Name == "Parse" &&
                        m.GetParameters().Count() == 1 &&
                        m.GetParameters()[0].ParameterType == typeof(string));

                lock (fParseMethodsLockObject)
                {
                    if (!ParseMethods.ContainsKey(type))
                    {
                        ParseMethods.Add(type, parseMethod);
                    }
                }
            }
            return parseMethod;
        }

        /// <summary>
        /// Materializes the specified entity.
        /// </summary>
        /// <param name="entity">The entity.</param>
        /// <param name="entryElement">The XML entry element that contains the data to use during materialization.</param>
        private void Materialize(object entity, XElement entryElement)
        {
            TTableStorageInfo tableInfo = this.TableInfoList.FirstOrDefault(ct => ct.ClassType == entity.GetType());

            if (tableInfo == null)
            {
                throw new TTableStorageException(String.Format("{0} is not a valid type", entity.GetType()));
            }

            List<TTablePropertyInfo> entityPropertyList = tableInfo.PropertyList;
            foreach (XElement propertyElement in entryElement.Element(atomNS + "content").Element(metaNS + "properties").Elements()
                .Where(e => entityPropertyList.Select(info => info.FieldName).ToList().Contains(e.Name.LocalName)))
            {
                PropertyInfo propInfo = entityPropertyList.Where(p => p.FieldName == propertyElement.Name.LocalName).FirstOrDefault().Info;
                object propValue;

                if (propInfo.PropertyType == typeof(string))
                {
                    propValue = propertyElement.Value;
                }
                else if (propInfo.PropertyType == typeof(Uri))
                {
                    propValue = new Uri(propertyElement.Value);
                }
                else if (propInfo.PropertyType.IsEnum)
                {
                    propValue = int.Parse(propertyElement.Value);
                }
                else if (propInfo.PropertyType == typeof(DateTime))
                {
                    propValue = DateTime.Parse(propertyElement.Value, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal);
                }
                else
                {
                    MethodInfo parseMethod = this.GetParseMethod(propInfo.PropertyType);

                    propValue = parseMethod.Invoke(null, new object[] { propertyElement.Value });
                }

                if (propInfo.PropertyType == typeof(DateTime) && ((DateTime)propValue) == TableStorageConstants.MinSupportedDateTime)
                {
                    propValue = DateTime.MinValue;
                }

                propInfo.SetValue(entity, propValue, null);
            }
        }

        /// <summary>
        /// Creates a basic table storage request.
        /// </summary>
        /// <param name="requestUri">The request URI.</param>
        /// <returns>A basic table storage request.</returns>
        /// <remarks>
        /// Creates a table storage request signed with our credentials and containing
        /// the standard table storage headers.  You must manually set the Method,
        /// Content-Type, Content-Length, and Content after creation.
        /// </remarks>
        private HttpWebRequest CreateRequest(Uri requestUri)
        {
            HttpWebRequest request = WebRequest.Create(requestUri) as HttpWebRequest;

            request.Date = DateTime.UtcNow;
            request.Accept = "application/atom+xml,application/xml";

            request.UserAgent = TableStorageConstants.UserAgent;
            request.Headers["DataServiceVersion"] = TableStorageConstants.DataServiceVersion1;
            request.Headers["MaxDataServiceVersion"] = TableStorageConstants.DataServiceVersion2;
            request.Headers["x-ms-version"] = TableStorageConstants.XMSVersion;
            request.Headers["Accept-Charset"] = TableStorageConstants.UTF8;

            this.Credentials.SignRequestLite(request);

            return request;
        }

        /// <summary>
        /// Creates the data service entry element (representing <paramref name="entity"/>) of a data service request.
        /// </summary>
        /// <param name="entity">The entity to describe in the entry element.</param>
        /// <returns>A new XElement containing the entry.</returns>
        private XElement CreateDataServiceEntry(ITableEntity entity)
        {
            XElement entry = new XElement(atomNS + "entry",
                new XAttribute(XNamespace.Xmlns + "d", dataNS),
                new XAttribute(XNamespace.Xmlns + "m", metaNS),
                new XAttribute("xmlns", atomNStext),

                new XElement(atomNS + "title"),

                new XElement(atomNS + "updated", DateTime.UtcNow.ToString(TableStorageConstants.DateTimeFormat)),

                new XElement(atomNS + "author",
                    new XElement(atomNS + "name")),

                new XElement(atomNS + "id"));

            entry.Add(this.CreateDataServiceContent(entity));

            return entry;
        }

        /// <summary>
        /// Converts object to a Data Services "content" element.
        /// </summary>
        /// <param name="entity">The object to serialize.</param>
        /// <returns>An XElement representing the content of the object.</returns>
        private XElement CreateDataServiceContent(ITableEntity entity)
        {
            XElement content = new XElement(atomNS + "content",
                new XAttribute("type", "application/xml"));

            XElement properties = new XElement(metaNS + "properties");
            content.Add(properties);

            TTableStorageInfo tableInfo = this.TableInfoList.Where(til => til.ClassType == entity.GetType()).FirstOrDefault();

            foreach (TTablePropertyInfo tablePropInfo in tableInfo.PropertyList)
            {
                string stringValue;

                if (tablePropInfo.Info.PropertyType.IsEnum)
                {
                    stringValue = ((int)tablePropInfo.Info.GetValue(entity, null)).ToString();
                }
                else
                {
                    if (tablePropInfo.Info.GetValue(entity, null) == null)
                    {
                        ////stringValue = null;
                        continue;
                    }
                    else
                    {
                        stringValue = tablePropInfo.Info.GetValue(entity, null).ToString();
                    }
                }

                if (tablePropInfo.Info.PropertyType == typeof(bool))
                {
                    stringValue = stringValue.ToLower();
                }
                else if (tablePropInfo.Info.PropertyType == typeof(DateTime))
                {
                    DateTime dateTime = (DateTime)tablePropInfo.Info.GetValue(entity, null);

                    if (dateTime == DateTime.MinValue)
                    {
                        dateTime = TableStorageConstants.MinSupportedDateTime;
                    }

                    stringValue = dateTime.ToString(TableStorageConstants.DateTimeFormat);
                }

                XElement propElement = new XElement(dataNS + tablePropInfo.FieldName,
                    new XAttribute(metaNS + "type", tablePropInfo.Info.PropertyType.GetEdmType()), stringValue);

                properties.Add(propElement);
            }

            // if the tableinfoList has a discriminator condition on one of its types, we need to save a discriminator property to the table
            if (this.TableInfoList.Count(ti => ti.ConditionalValue > 0) >= 1)
            {
                XElement discriminatorElement = new XElement(dataNS + TableStorageConstants.DiscriminatorFieldName,
                    new XAttribute(metaNS + "type", TableStorageConstants.DiscriminatorFieldType),
                    tableInfo.ConditionalValue);

                properties.Add(discriminatorElement);
            }

            return content;
        }

        /// <summary>
        /// Gets the response to <paramref name="request"/> and increments a transaction counter.
        /// </summary>
        /// <param name="request">The request.</param>
        /// <returns>The response to <paramref name="request"/>.</returns>
        private HttpWebResponse TrackGetResponse(HttpWebRequest request)
        {
            if (this.TransactionCountIncremented != null)
            {
                this.TransactionCountIncremented();
            }

            return (HttpWebResponse)request.GetResponse();
        }
    }
}
